package Verifier;

import java.io.IOException;
import java.nio.ByteBuffer;
import ArithmeticObjects.*;
import ArithUtils.*;
import CryptoPrim.*;
import FiatShamirProofs.*;
import Utils.*;

/**
 * Main class of the verifier. Implements the verification algorithm.
 * 
 * @author Itamar Carmel, nirhagin
 */
public class Main {
	
	// program fields
	private static ProtocolInfo protInfo = null; // holds protocol info data
	private static ParsedVerifierInput verifierInput = null; // holds verifier args
	private static ProofDirectoryParam proofDir = null; // holds proof directory data
	private static MyLogger logger; // logger (writer to log file)
	
	private static String COMPAT_OPT = "-compat"; // 'compat' command string rep.
	private static String[] VERSIONS = {"version1.0","version2.0","version2.5"}; // list of versions (made up for the time being)
	
	private static int ERROR = -1; // error exit status
	private static int SUCCESS = 0; // success exit status
	
	/**
	 * Main static method of the verfier.
	 * Inputs command line args. 
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		/////////////////////////////////
		// Step 1: Protocol parameters //
		/////////////////////////////////
	
		if (args[0].equals(COMPAT_OPT)){
			if (args.length > 1){
				printUsage();
				exit(ERROR);
			}
			printVersions();
			exit(SUCCESS);
		}
		
		try {
			verifierInput = new ParsedVerifierInput(args);
		} catch (Exception e1) {
			printUsage();
			exit(ERROR);
		}
		
		try {
			logger = new MyLogger(
					verifierInput.getDirectoryPath(),
					verifierInput.isV());
		} catch (IOException e1) {
			System.err.println("Log file initiation failed. Program will not exit.");
		}
		
		logger.writeHeader("VERIFIER INITIATED");
		logger.write("Verifier inputs parsed successfuly.\n" + verifierInput);
		logger.writeHeader("STEP 1: Protocol parameters");
		
		try {
			protInfo = new ProtocolInfo(verifierInput.getProtInfoPath());
		} catch (Exception e2) {
			logger.writeError("Loading of protocol information file failed.");
			exit(ERROR);
		}
		logger.write("Protocol parameters readed successfully.\n" + protInfo.toString());

		//////////////////////////////
		// Step 2: Proof parameters //
		//////////////////////////////
		logger.writeHeader("STEP 2: Proof parameters");
		try {
			proofDir = new ProofDirectoryParam(verifierInput.getDirectoryPath());
		} catch (Exception e1) {
			logger.writeError("Loading of proof directory failed.");
			exit(ERROR);
		}
		logger.write("Proof parameters readed successfully.");
		if (!verifyProofParam()){
			logger.writeError("Verifying of proof parameters failed.");
			exit(ERROR);
		}
		logger.resetLevel();
		logger.write("Proof parameters verified.");
		
		///////////////////////////////////////
		// Step 3: Deriving sets and objects //
		///////////////////////////////////////
		logger.writeHeader("STEP 3: Deriving sets and objects");
		logger.write("Starting derivation.");
		logger.incrementLevel();
		
		VerfGroup Gq = null;
		BTField<BigNumber> Zq = null;
		PlaintextGroup Mw = null;
		CiphertextGroup Cw = null;
		RandomizerGroup Rw = null;
		HashFunction H = null;
		PRG prg = null;
		
		Gq = protInfo.getGroup();
		if (Gq == null){
			logger.writeError("Group derivation failed.");
			exit(ERROR);}
		logger.write("Group derivation was successful.");
		
		Zq = staticTypes.getNumberField(Gq.getOrder());
		if (Zq == null){
			logger.writeError("Field derivation failed.");
			exit(ERROR);}
		logger.write("Field derivation was successful.");
		
		Mw = new PlaintextGroup(Gq,proofDir.getWidth());
		logger.write("Plaintext Group derivation was successful.");
		
		Cw = new CiphertextGroup(Gq,proofDir.getWidth());
		logger.write("Ciphertext Group derivation was successful.");
		
		Rw = new RandomizerGroup(Zq,proofDir.getWidth());
		logger.write("Randomizer Group derivation was successful.");
		
		H = new HashFunction(protInfo.getHashfunction());
		logger.write("Hash Function derivation was successful.");
		
		prg = new PRG(new HashFunction(protInfo.getPRG()));
		logger.write("PRG derivation was successful.");
		
		logger.resetLevel();
		logger.write("Sets and objects derived successfully.");
		
		//////////////////////////////////////////////
		// Step 4: Compute prefix to Random Oracles //
		//////////////////////////////////////////////
		logger.writeHeader("STEP 4: Compute prefix to Random Oracles");
		
		ByteTree bt = ByteTree.generateEmptyNode();
		bt.addMultChildren(
				ByteTree.generateNewLeaf(proofDir.getVersion()),
				ByteTree.generateNewLeaf(protInfo.getSid() + "." + proofDir.getAuxsid()),
				ByteTree.generateNewLeaf(ByteBuffer.allocate(4).putInt(protInfo.getNr()).array()),
				ByteTree.generateNewLeaf(ByteBuffer.allocate(4).putInt(protInfo.getNv()).array()),
				ByteTree.generateNewLeaf(ByteBuffer.allocate(4).putInt(protInfo.getNe()).array()),
				ByteTree.generateNewLeaf(protInfo.getPRG()),
				ByteTree.generateNewLeaf(protInfo.getGroupStr()),
				ByteTree.generateNewLeaf(protInfo.getHashfunction()));
		byte[] rho = H.produceDigest(bt.getByteRepOfTree());
		logger.write("Prefix to Random Oracles computed successfully.");
		
		///////////////////////
		// Step 5: Read keys //
		///////////////////////
		logger.writeHeader("STEP 5: Read keys");
		
		// 5.1. Joint public key
		logger.write("Loading joint public key.");
		PubKey pk = proofDir.getFullPublicKey(Gq);
		if (pk == null){
			logger.writeError("Reading of public key from proof directory failed.");
			exit(ERROR);
		}
		logger.write("Public key loaded successfully.");
		
		// 5.2. Public keys
		logger.write("Loading public keys.");
		logger.incrementLevel();
		Array<VerfGroup.VElement> y = new Array<VerfGroup.VElement>();
		VerfGroup.VElement yTemp;
		for (int l = 1; l <= protInfo.getLambda(); l++){
			yTemp = proofDir.getPartialPublicKey(l,Gq);
			if (yTemp == null){
				logger.writeError("Attempt to read the " + l + " partial public key failed.");
				exit(ERROR);
			}
			logger.write("Attempt to read the " + l + " partial public key was successful.");
			y.add(yTemp);
		}
		logger.resetLevel();
		logger.write(protInfo.getLambda() + " partial public keys read successfuly.");
		
		VerfGroup.VElement yPI = y.get(0);
		for (int l = 1; l < protInfo.getLambda(); l++){
			yPI = Gq.mult(yPI,y.get(l));
		}
		if (!Gq.ElementIsEqual(pk.getY(),yPI)){
			logger.writeError("Partial public keys are inconsistent.");
			exit(ERROR);
		}
		logger.write("Public keys consistency verified.");
		
		// 5.3. Secret keys
		logger.write("Loading secret keys.");
		logger.incrementLevel();
		Array<SecKey> x = new Array<SecKey>();
		SecKey sk = null;
		for (int l = 1; l <= protInfo.getLambda(); l++){
			try{
				sk = proofDir.getSecretKey(l,Zq);
			} catch (Exception e){
				logger.writeError(l + "'th secret key is not in the correct format.");
				exit(ERROR);
			}
			if (sk == null){
				logger.write(l + "'th secret key loaded successfuly.");
				x.add(null);
				continue;
			}
			if (!Gq.ElementIsEqual(y.get(new BigNumber(l)),Gq.pow(pk.getG(),sk.getX().getValue()))){
				logger.writeError(l + "'th secret key is not consistent with his parallel public key.");
				exit(ERROR);
			}
			logger.write(l + "'th secret key loaded successfuly.");
			x.add(sk);
		}
		logger.resetLevel();
		logger.write(protInfo.getLambda() + " secret keys read and verified successfuly.");
		
		////////////////////////
		// Step 6: Read lists //
		////////////////////////
		logger.writeHeader("STEP 6: Read lists of ciphertexts and plaintexts");
		
		// 6.a. Read input ciphertexts
		logger.write("Read input ciphertexts.");
		Array<Ciphertext> L0 = proofDir.getInputCiphertexts(Cw);
		if (L0 == null){
			logger.writeError("Loading input ciphertexts failed.");
			exit(ERROR);
		}
		logger.write("Input ciphertexts loaded successfully.");
		int N = TypeConvertionUtils.bigNumberToInt(L0.getLength());
		logger.write("N (number of ciphertexts) is set to be - " + N);
		
		// 6.b. Read shuffled ciphertexts
		Array<Ciphertext> Llambda = null;
		if (verifierInput.getType().equals("mixing")){
			logger.write("Read shuffled ciphertexts.");
			Llambda = proofDir.getCiphertexts(protInfo.getLambda(),Cw);
		}
		else if (verifierInput.getType().equals("shuffling")){
			logger.write("Read shuffled ciphertexts.");
			Llambda = proofDir.getShuffledCiphertexts(Cw);
		}
		
		// 6.c. Read plaintexts
		Array<Plaintext> m = new Array<Plaintext>();
		if ((verifierInput.getType().equals("mixing")) ||
				(verifierInput.getType().equals("decryption"))){
			logger.write("Read plaintexts.");
			m = proofDir.getPlaintexts(Mw);
		}
		
		logger.write("Shuffled ciphertexts / plaintexts loaded successfully.");	
		
		////////////////////////////////////////////
		// Step 7: Verify relations between lists //
		////////////////////////////////////////////
		
		logger.writeHeader("STEP 7: Verify relations between lists");
		logger.write("Initiating seed and challenge Random Oracles.");
		RandomOracle ROSeed = new RandomOracle(H,prg.getSeedLen());
		RandomOracle ROChallenge = new RandomOracle(H,protInfo.getNv());
		ByteTree tau;
		ByteTree sigma;
		
		// Compute h - derive random elements of Gq
		logger.write("Deriving random elements in Gq.");
		byte[] seed = ProofUtils.computeSeed(ROSeed,rho,null);
		prg.setSeed(seed);
		Array<VerfGroup.VElement> h = Gq.randomArray(N,prg,protInfo.getNr());
		
		
		// 7.a. Verify shuffling
		if (
				((verifierInput.getType().equals("mixing")) ||
				(verifierInput.getType().equals("shuffling"))) &&
				(verifierInput.getPosc() || verifierInput.getCcpos())){
			logger.write("Verify shuffling.");
			logger.incrementLevel();
			logger.write("Attempt to read 'maxciph' from proof directory.");
			int N0 = proofDir.getMaxciph();
			Array<VerfGroup.VElement> ul = null;
			Array<Ciphertext> Lpre = null;
			Array<Ciphertext> Lcur = null;
			
			// 7.a.I. In case 'maxciph' file does not exist
			if (N0 < 0){
				logger.write("'maxciph' does not exists.");
				logger.incrementLevel();
				Lpre = L0;
				ProofOAS pOAS;
				
				logger.write("Commencing verification of shuffling for each one of the " + protInfo.getLambda() + "'th mix-server.");
				logger.incrementLevel();
				for (int l = 1; l <= protInfo.getLambda(); l++){
					// 7.a.I.1. Array of ciphertexts
					if (l < protInfo.getLambda()){
						Lcur = proofDir.getCiphertexts(l,Cw);
						if (Lcur == null){
							logger.writeError("Attempt to read the " + l + "'th ciphertexts failed.");
							exit(ERROR);
						}
					} else
						Lcur = Llambda;
					logger.write("Attempt to read the " + l + "'th ciphertexts was successful.");
					
					// 7.a.I.2. Verify proof of shuffle
					tau = proofDir.getPoSCommitment(l);
					sigma = proofDir.getPoSReply(l);
					ul = proofDir.getPermutationCommitment(l,Gq);
					pOAS = new ProofOAS(
							ROSeed,ROChallenge,rho,N,protInfo.getNe(),
							protInfo.getNr(),protInfo.getNv(),prg,Gq,
							Rw,Cw,pk,Lpre,Lcur,ul,tau,sigma,h);
					if (
							(ul == null) || (tau == null) ||
							(sigma == null) || (!pOAS.verifyProof())){
						for (int i = 0; i < N; i++){
							if (!Cw.ciphertextIsEqual(Lcur.get(i),Lpre.get(i))){
								logger.writeError("Attempt to verify the " + l + "'th mix-server proof of shuffle failed.");
								exit(ERROR);
							}
						}
					}
					
					// 7.a.I.3. Accept proof
					logger.write("Proof of shuffle for the " + l + "'th mix-server has been verified.");
					Lpre = Lcur;
				}
				logger.decrementLevel();
				logger.write("All " + protInfo.getLambda() + " mix-servers' shuffling verified.");
				logger.decrementLevel();
			}
			// 7.a.II. In case 'maxciph' file exists
			else{
				logger.write("'maxciph' exists.");
				logger.incrementLevel();
				if (N0 < N){
					logger.writeError("Current amount of ciphers (N) is larger then the maximum(N0).");
					exit(ERROR);
				}
				prg.setSeed(seed);
				Array<VerfGroup.VElement> h0 = Gq.randomArray(N0,prg,protInfo.getNr());
				Lpre = L0;
				ProofOASOC pOASOC;
				ProofCCPOAS pCCPOAS;
				Array<MyBoolean> tl;
				Array<MyBoolean> tDefault = MyBoolean.setDefaultArray(N0,N);
				Array<VerfGroup.VElement> uTemp;
				
				logger.write("Commencing verification of shuffle of commitments and commitment-consistent proofs" +
						" for each one of the " + protInfo.getLambda() + "'th mix-server.");
				logger.incrementLevel();
				for (int l = 1; l <= protInfo.getLambda(); l++){
					// 7.a.II.1. Verify proof of SOC
					if (verifierInput.getPosc()){
						ul = proofDir.getPermutationCommitment(l,Gq);
						tau = proofDir.getPoSCCommitment(l);
						sigma = proofDir.getPoSCReply(l);
						pOASOC = new ProofOASOC(
								ROSeed,ROChallenge,rho,N0,protInfo.getNe(),
								protInfo.getNr(),protInfo.getNv(),prg,Gq,
								ul,tau,sigma,h0);
						if (
									(ul == null) || (tau == null) ||
									(sigma == null) || (!pOASOC.verifyProof())){
							ul = h0;
						}
					}
					
					// 7.a.II.2. Potential early abort
					if (!verifierInput.getCcpos()){
						logger.write("Early abort on the " + l + "'th mix-server: ccpos = false.");
						continue;
					}
					
					// 7.a.II.3. Shrink permutation commitment
					tl = proofDir.getKeepList(l);
					if (tl == null){
						tl = tDefault;
					}
					uTemp = new Array<VerfGroup.VElement>();
					for (int i = 0; i < N0; i++){
						if (tl.get(i).isTrue())
							uTemp.add(ul.get(i));
					}
					ul = uTemp;
					
					// 7.a.II.4. Array of ciphertexts
					if (l < protInfo.getLambda()){
						Lcur = proofDir.getCiphertexts(l,Cw);
						if (Lcur == null){
							logger.writeError("Reading of the " + l + "'th mix-server's ciphertexts failed.");
							exit(ERROR);
						}
					} else
						Lcur = Llambda;
					
					// 7.a.II.5. Verify commitment-consistent proof of shuffle

					tau = proofDir.getCCPoSCommitment(l);
					sigma = proofDir.getCCPoSReply(l);
					pCCPOAS = new ProofCCPOAS(
							ROSeed,ROChallenge,rho,N,protInfo.getNe(),
							protInfo.getNr(),protInfo.getNv(),prg,Gq,
							ul,Rw,Cw,Lpre,Lcur,pk,tau,sigma,h);
					if (
							(sigma == null) || (tau == null) ||
							(!pCCPOAS.verifyProof())){
						for (int i = 0; i < N; i++){
							if (!Cw.ciphertextIsEqual(Lcur.get(i),Lpre.get(i))){
								logger.writeError("Attempt to verify the " + l + "'th mix-server " +
										"commitment-consistent proof of shuffle failed.");
								exit(ERROR);
							}
						}
					}
					
					// 7.a.II.6. Accept proof
					logger.write("Attempt to verify the " + l + "'th mix-server " +
							"proof of shuffle of commitments and " +
							"commitment-consistent proof of shuffle was successful.");
					Lpre = Lcur;
				}
				logger.decrementLevel();
				logger.write("All " + protInfo.getLambda() + " mix-servers' " +
						"shuffle of commitments and commitment-consistent " +
						"proof of shuffle verified.");
				logger.decrementLevel();
			}
			
			logger.decrementLevel();
		}

		// 7.b. Verify decryption
		Array<Ciphertext> L = new Array<Ciphertext>();
		
		if (verifierInput.getDec()){
			logger.write("Verify decryption");
			logger.incrementLevel();
			if (verifierInput.getType().equals("mixing"))
				L = Llambda;
			else if (verifierInput.getType().equals("decryption"))
				L = L0;
			else{
				logger.writeError("Verifer inputs do not match none of " +
						"the proofs.");
				exit(ERROR);
			}
			
			// 7.b.1. Read proofs
			logger.write("Read proofs.");
			Array<Array<Plaintext>> f = new Array<Array<Plaintext>>();
			tau = ByteTree.generateEmptyNode();
			sigma = ByteTree.generateEmptyNode();
			Array<Plaintext> tempF;
			ByteTree tempTau;
			ByteTree tempSigma;
			logger.incrementLevel();
			for (int l = 1; l <= protInfo.getLambda(); l++){
				tempF = proofDir.getDecryptionFactors(l,Mw);
				tempTau = proofDir.getDecrFactCommitment(l);
				tempSigma = proofDir.getDecrFactReply(l);
				if ((tempF == null) || (tempTau == null) || (tempSigma == null)){
					logger.writeError("Reading of " + l + "'th decryption parameters failed.");
					exit(ERROR);
				}
				f.add(tempF);
				tau.addChild(tempTau);
				sigma.addChild(tempSigma);
				logger.write("Reading of " + l + "'th decryption parameters was successful.");
			}
			logger.decrementLevel();
			logger.write("All of the " + protInfo.getLambda() +
					" decryption proofs read successfuly.");
			
			// 7.b.2. Verify combined proof
			logger.write("Verify combined proof.");
			ProofOCD pOCD = new ProofOCD(
					ROSeed,ROChallenge,rho,N,protInfo.getNe(),
					protInfo.getNr(),protInfo.getNv(),prg,Gq,
					y,Cw,Mw,L,f,tau,sigma,protInfo.getLambda());
			if (!pOCD.verifyProof(0)){
				// 7.b.3. Verify individual proofs
				logger.write("Combined proof verification failed.");
				logger.write("Verify individual proofs.");
				logger.incrementLevel();
				Plaintext pdecL;
				for (int l = 1; l <= protInfo.getLambda(); l++){
					if (!pOCD.verifyProof(l)){
						if (x.get(l-1) == null){
							logger.writeError("Decryption proof for " + l +
									"'th mix-server failed.");
							exit(ERROR);
						}
						for (int i = 0; i < N; i++){
							pdecL = ProofUtils.PDec(x.get(new BigNumber(l-1)),L.get(i),Mw);
							if (!Mw.plaintextIsEqual(f.get(l-1).get(i),pdecL)){
								logger.writeError("Decryption proof for " + l +
										"'th mix-server failed.");
								exit(ERROR);
							}
						}
					}
					logger.write("Individual decryption proof for " + l +
							"'th mix-server was successful.");
				}
				logger.decrementLevel();
			} else{
				logger.write("Combined proof verified successfuly.");
			}
			
			// 7.b.4. Verify plaintexts
			logger.write("Verify plaintexts.");
			logger.incrementLevel();
			Plaintext tdecL;
			Array<Plaintext> PIf = new Array<Plaintext>();
			Plaintext tempPT;
			for (int i = 0; i < N; i++){
				tempPT = f.get(0).get(i);
				for (int l = 1; l < protInfo.getLambda(); l++){
					tempPT = Mw.multiply(tempPT,f.get(l).get(i));
				}
				PIf.add(tempPT);
			}
			for (int i = 0; i < N; i++){
				tdecL = ProofUtils.TDec(L.get(i),PIf.get(i),Mw);
				if (!Mw.plaintextIsEqual(m.get(i),tdecL)){
					logger.writeError("Plaintext " + (i+1) + " verefication failed.");
					exit(ERROR);
				}
			}
			logger.write("Plaintexts verified successfuly.");
			logger.decrementLevel();
			// 7.b.5. Accept proof
			
			logger.write("Verification of decryption was successfull.");
			logger.decrementLevel();
		}
		
		// 7.c. Accept Proof
		exit(SUCCESS);
	}

	/**
	 * Exit method from verifer which -
	 * (1) closes logger.
	 * (2) exists with given status.
	 * 
	 * @param status (exit status)
	 */
	private static void exit(int status) {
		if (logger != null){
			logger.resetLevel();
			logger.writeHeader("VERIFIER TERMINATED - STATUS: " + status);
			try {
				logger.close();
			} catch (IOException e) {
				System.err.println("ERROR: Failed to close log file.");
			}
		}
		System.exit(status);
	}

	/**
	 * Helper method which verifies proof parameters.
	 * 
	 * @return true - parameters verified; false - otherwise.
	 */
	private static boolean verifyProofParam() {
		logger.write("Verifying proof parameters.");
		logger.incrementLevel();
		if (!proofDir.getVersion().equals(protInfo.getVersion())){
			logger.writeError("Version verification failed.");
			return false;
		}
		logger.write("Version verification was successful.");
		if (!proofDir.getType().equals(verifierInput.getType())){
			logger.writeError("Type verification failed.");
			return false;
		}
		logger.write("Type verification was successful.");
		if (!proofDir.getAuxsid().equals(verifierInput.getAuxsid())){
			logger.writeError("Auxsid verification failed.");
			return false;
		}
		logger.write("Auxsid verification was successful.");
		if ((verifierInput.getWidth() == -1) &&
				(proofDir.getWidth() != protInfo.getWidth())){
			logger.writeError("#1 Width verification failed.");
			return false;
		}
		logger.write("#1 Width verification was successful.");
		if ((verifierInput.getWidth() != -1) &&
				(proofDir.getWidth() != verifierInput.getWidth())){
			logger.writeError("#2 Width verification failed.");
			return false;
		}
		logger.write("#2 Width verification was successful.");
		return true;
	}

	/**
	 * Print method of command line args proper usage.
	 */
	private static void printUsage() {
		System.out.println("Correct verifier usage:");
		System.out.println("MIXING -\tverifier -mix <protInfo.xml_path> <directory_path>");
		System.out.println("\t[-auxsid <auxsid>] [-width <width>] [-nopos] [-noposc] [-noccpos] [-nodec]");
		System.out.println("SHUFFLE -\tverifier -shuffle <protInfo.xml_path> <directory_path>");
		System.out.println("\t[-auxsid <auxsid>] [-width <width>] [-nopos] [-noposc] [-noccpos]");
		System.out.println("DECRYPTION -\tverifier -decrypt <protInfo.xml_path> <directory_path>");
		System.out.println("\t[-auxsid <auxsid>] [-width <width>]");
	}

	/**
	 * Print method of all possible versions which are supported by this verifier.
	 */
	private static void printVersions() {
		System.out.println("================================");
		System.out.println("COMPATIBLE VERIFICATUM VERSIONS:");
		for (String ver : VERSIONS)
			System.out.println(ver);
		System.out.println("================================");
	}
}